package quickup;

/**
 *  Copyright 2008 Jeffrey Ravenhorst <butjeffsays@gmail.com>
 * 
 *  ====================================================================
 *  This file is part of QuickUp.
 *
 *  QuickUp is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  QuickUp is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with QuickUp. If not, see <http://www.gnu.org/licenses/>.
 *  
 *  QuickUp makes partial use of the json.org json to java converter.
 *  The licence information of those portions of code can be found in
 *  JSONObject.java or at <http://www.json.org/license.html>.
 *  ====================================================================
 *
 *  QuickUp is in an Alpha testing stage.
 */

import netscape.javascript.*;

import java.applet.Applet;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.ArrayList;
import java.util.List;

import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTable;

public class QuickUp extends Applet implements DropTargetListener, ActionListener, ProgressListener {

	private static final long serialVersionUID = 1L;
	
	private JTable list;
	private FileUploadTableModel data;
	private JButton uploadButton;
	private JButton clearButton;
	private ArrayList<File> fileList = new ArrayList<File>();
	private Thread uploaderThread;
	private Thread progressWatcher;
	private JProgressBar progressBar = new JProgressBar();
	private AbstractFileFilter filter;
	private boolean cancelled = false;
	private boolean isUploading = false;
	private boolean blockDrop = false;
	private String field_name = null;
	private String form_data = "";
	private JSObject browser = null;
	private QuickUp self = this;
	private final ArrayList<Thread> uploadThreads = new ArrayList<Thread>();
	
	public void init() {
		try {
			boolean debug = getParameter("debug").equalsIgnoreCase("true");
			Debug.setEnabled(debug);
			Debug.outln("Debug Mode");
		} catch (Exception e){
			// do nothing;
		}
		
		// get the external form post data
		setFormData(getParameter("form_data"));
		
		// attempt to turn on javascript interaction
		try {
			browser = JSObject.getWindow(this);
			callJavascript("onQuickUpInit",null);
		} catch (Exception e) {
			Debug.showWarning("Javascript interaction unavailable.");
		}
		
		// build the GUI
		data = new FileUploadTableModel();
		list = new JTable(data);
		list.setCellSelectionEnabled(false);
		list.setShowHorizontalLines(false);
		list.setDefaultRenderer(JProgressBar.class, new AdvancedCellRenderer());
		list.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
		list.getColumn("").setMaxWidth(20);
		list.getColumn("Size").setMaxWidth(50);
		list.getColumn("Progress").setMinWidth(100);
		list.getColumn("Status").setMinWidth(200);
		
		// This method is not available in 1.5 compliance mode
		// so we'll just set the background of the viewport to 
		// white so that it looks better.
		// list.setFillsViewportHeight(true);
		
		try {
			filter = new AbstractFileFilter(getParameter("filetypes").split(","));
		} catch (Exception e) {
			filter = new AbstractFileFilter();
		}
		
		field_name = getParameter("field_name");
		if(field_name == null)
			field_name = "uploadfile";
				
		JScrollPane scrollPane = new JScrollPane(list);
		scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
		scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
		scrollPane.getViewport().setBackground(Color.WHITE);
		
		@SuppressWarnings("unused")
		DropTarget dt = new DropTarget(scrollPane,this);
		list.setDropTarget(null);
		
		Dimension buttonSize = new Dimension(100,35);
		uploadButton = new JButton("Upload");
		uploadButton.addActionListener(this);
		uploadButton.setPreferredSize(buttonSize);
		uploadButton.setEnabled(false);
		clearButton = new JButton("Clear");
		clearButton.addActionListener(this);
		clearButton.setPreferredSize(buttonSize);
		clearButton.setEnabled(false);
		
		JPanel buttons = new JPanel();
		buttons.setLayout(new BorderLayout(3,3));
		buttons.add(uploadButton, BorderLayout.WEST);
		buttons.add(clearButton, BorderLayout.EAST);
		buttons.add(progressBar, BorderLayout.CENTER);
		
		progressBar.setStringPainted(true);
		
		setLayout(new BoxLayout(this,BoxLayout.Y_AXIS));
		add(scrollPane);
		add(buttons);
	}

	public void dragEnter(DropTargetDragEvent arg0) {
		// ignore event
	}

	public void dragOver(DropTargetDragEvent arg0) {
		// ignore event
	}

	public void dropActionChanged(DropTargetDragEvent arg0) {
		// ignore event
	}

	public void dragExit(DropTargetEvent arg0) {
		// ignore event
	}

	public void drop(DropTargetDropEvent dtde) {
		Debug.outln("Drop event recorded.");
		if(blockDrop) return;
		int action = dtde.getDropAction();
		dtde.acceptDrop(action);
		addFilesFromDrop(dtde.getTransferable(),dtde);
	}

	public void actionPerformed(ActionEvent e) {
		if (e.getSource() == uploadButton) {
			uploadFiles();
		} else if (e.getSource() == clearButton) {
			if (isUploading) {
				onUploadCancel();
			} else {
				onUploadClear();
			}
		}
	}

	// add the files from the drop event to the data list.
	private void addFilesFromDrop(Transferable t,DropTargetDropEvent dtde) {
		if (t == null) return;
		try {
			if (t.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
				List files = (List) t.getTransferData(DataFlavor.javaFileListFlavor);
				// release the drop event so the mouse doesnt seem to 'lock up'
				// while files are being added.
				dtde.dropComplete(true);
				addFiles(files);
			} else {
				DataFlavor flavors[] = t.getTransferDataFlavors();
				DataFlavor df = DataFlavor.selectBestTextFlavor(flavors);
				String mimeType = df.getMimeType();
				Debug.showError("Invalid Mimetype: " + mimeType);
				dtde.dropComplete(true);
			}
		} catch (NullPointerException npe) {
			System.out.println(npe.getMessage());
		} catch (Exception ex) {
			System.out.println(ex.getMessage());
		} finally {
			dtde.dropComplete(true);
		}
	}
	
	private synchronized void addFiles(List importlist) {
		// only add to the list if not already in the list.
		for (int i = 0; i < importlist.size(); i++) {
			File file = (File) importlist.get(i);
			if (filter.accept(file)) {
				boolean exists = false;
				for (int j = 0; j < fileList.size(); j++) {
					if (file.getAbsolutePath().equals(
							fileList.get(j).getAbsolutePath())) {
						exists = true;
						break;
					}
				} if (!exists) fileList.add(file);
			}
		}

		// if a folder was added to the file list, recursively remove it
		// and add all files within
		for (int i = 0; i < fileList.size(); i++) {
			File f = (File) fileList.get(i);
			if (f.isDirectory()) {
				File[] within = f.listFiles(filter);
				fileList.remove(i--);
				for (int j = 0; j < within.length; j++) {
					fileList.add(within[j]);
				} continue;
			}					
		}
		
		// if the fileList has increased, add the new files to the file table
		if (fileList.size() > data.getRowCount()) {
			//System.out.println("adding " + fileList.size() + " files.");
			for(int i = data.getRowCount(); i < fileList.size(); i++) {
				data.add(new FileUpload(fileList.get(i)));
			} clearButton.setText(" Clear ");
			uploadButton.setEnabled(true);
			clearButton.setEnabled(true);
		}
	}

	private void uploadFiles() {
		// default number of maximum concurrent uploads
		int maxUp = 5;
		try {
			maxUp = Integer.valueOf(getParameter("max_uploads"));
		} catch (Exception e) {
			showWarning("Parameter 'max_uploads' not recognized.");
		} final int maxUploads = maxUp; 
		
		// http addr to the server-side upload script
		String uploadPath = getParameter("destination");
		if(uploadPath == null) {
			showError("Parameter 'destination' not set.");
			return;
		} Debug.outln("Uploading to: " + uploadPath);
		
		HttpDestination dest;
		boolean authreq = getParameter("authorization").equalsIgnoreCase("basic");
		
		if(authreq) {
			String[] auth = requestAuth();
			if(auth!=null) {
				dest = new HttpDestination(uploadPath,auth[0],auth[1]);
			} else {
				showError("Authorization Required.");
				return;
			}
		} else {
			dest = new HttpDestination(uploadPath);
		} final HttpDestination script = dest;
		
		// turn off the upload button while the uploads begin.
		// and change the clearButton to cancel.
		onUploadBegin();
		
		// list of files (that are checked)
		final ArrayList<FileUpload> uploads = data.getFileUploads();
		
		// this thread keeps track of finished uploads
		// and makes sure the maximum upload limit is respected
		uploaderThread = new Thread(new Runnable() {
			public void run() {
				Debug.outln("uploaderThread running...");
				for(int i = 0; i < uploads.size(); i++) {
					upload(uploads.get(i));
				} // Debug.outln("uploaderThread finished.");
			}
			private void upload(FileUpload fu) {
				if(cancelled) {
					fu.onStatusChange(null,"Cancelled"); return;
				} if (uploadThreads.size() >= maxUploads) {
					try {
						Thread.sleep(1000);
					} catch (InterruptedException ie) {
						fu.onStatusChange(null,"Cancelled");
						return;
					} upload(fu); // recursively attempt upload again
				} else {
					JSONObject json = null;
					if(form_data != null && !form_data.equals("")) {
						try {
							json = new JSONObject(form_data);
						} catch (JSONException je) {
							showWarning("JSON not recognized:\n\"" +
									form_data + "\"\n" + je.getMessage());
						}
					} HttpFileUploader temp = new HttpFileUploader(fu.getFile(),field_name,json,script);
					temp.addProgressListener(fu);
					temp.addProgressListener(self);
					uploadThreads.add(temp);
					temp.start();
				}
			}
		}); uploaderThread.start();
		
		// this thread updates the progress bars and status
		// messages until upload threads have terminated
		progressWatcher = new Thread(new Runnable() {
			ArrayList<FileUpload> activeUploads = data.getFileUploads();
			int total = 0;
			int progress = 0;
			public void run() {
				Debug.outln("progressWatcher running...");
				// get a total number of bytes for the upload
				for(int i = 0; i < activeUploads.size(); i++) {
					total += activeUploads.get(i).getFileSizeBytes();
				} progressBar.setMaximum(total);
				// update the progress and status columns
				int col = data.findColumn("Progress");
				// don't stop the thread until all uploads have completed
				// and poll for progress every 0.25 seconds
				while(uploadThreads.size() > 0 || uploaderThread.isAlive()) {
					updateProgress(col);
					try {Thread.sleep(250);} catch (Throwable t) {break;}
				} updateProgress(col);
				onUploadComplete();
				Debug.outln("progressWatcher finished.");
			}
			public void updateProgress(int col) {
				for(int row = 0; row < data.getRowCount(); row++) {
					data.fireTableCellUpdated(row,col);
					data.fireTableCellUpdated(row,col+1); // status
					int done = 0;
					for(int i = 0; i < activeUploads.size(); i++) {
						done += activeUploads.get(i).getProgressBar().getValue();
					} if(done != progress) {
						progressBar.setValue(done);
						callJavascript("onUploadProgressChange",new Object[] {done,total});
					} progress = done;
				}
			}
		}); progressWatcher.start();
	}

	private Object callJavascript(String method, Object[] params) {
		Object result = null;
		try {
			if(browser != null) result = browser.call(method,params);
		} catch (Exception e) {
			// the method wasnt found.
		} return result;
	}

	private void onUploadBegin() {
		isUploading = true;
		setDragAndDropEnabled(false);
		uploadButton.setEnabled(false);
		clearButton.setText("Cancel");
		callJavascript("onUploadBegin",null);
	}
	
	private void onUploadComplete() {
		isUploading = false;
		clearButton.setText(" Clear ");
		clearButton.setEnabled(true);
		callJavascript("onUploadComplete",null);
	}
	
	private void onUploadCancel() {
		cancelled = true;
		if (uploaderThread != null && uploaderThread.isAlive()) {
			uploaderThread.interrupt();
			clearButton.setEnabled(false);
			clearButton.setText(" Clear ");
		} callJavascript("onUploadCancel",null);
	}
	
	private void onUploadClear() {
		fileList.clear();
		data.clear();
		cancelled = false;
		progressBar.setValue(0);
		uploadButton.setEnabled(false);
		clearButton.setEnabled(false);
		setDragAndDropEnabled(true);
		callJavascript("onUploadClear",null);
	}
	
	public void setDragAndDropEnabled(boolean b) {
		blockDrop = !b;
		list.setEnabled(b);
	}
	
	public void setFormData(String json) {
		Debug.outln("Form Data set:");
		Debug.outln(json);
		form_data = json;
	}
	
	public String getFormData() {
		return form_data;
	}
	
	public static void showError(String string) {
		JOptionPane.showMessageDialog(null,string,"Error",JOptionPane.ERROR_MESSAGE);
	}

	public static void showWarning(String string) {
		JOptionPane.showMessageDialog(null,string,"Warning",JOptionPane.WARNING_MESSAGE);
	}
	
	public static String[] requestAuth() {
		AuthorizationDialog prompt = new AuthorizationDialog(null);
		prompt.setVisible(true);
		int value = prompt.getValue();
		String[] auth = null;
		if (value == JOptionPane.OK_OPTION)
			auth = prompt.getAuth();
		prompt.dispose();
		return auth;
	}

	public void onProgressChange(Object origin, int position, int total) {
		try {
			if(browser != null) {
				String file = ((HttpFileUploader)origin).getFileName();
				callJavascript("onFileProgressChange", new Object[] {file,position,total});
			}
		} catch (ClassCastException cce) {
			// ignore errors
		}
	}

	public void onStatusChange(Object origin, String status) {
		try {
			if(browser != null) {
				String file = ((HttpFileUploader)origin).getFileName();
				callJavascript("onFileStatusChange", new String[] {file,status});
			}
		} catch (ClassCastException cce) {
			// ignore errors
		}
	}

	// method synchronized because it is destructive to 'uploadThreads'
	public synchronized void onOperationFinished(Object origin) {
		Debug.outln(origin + " finished.");
		uploadThreads.remove(origin);
	}
}
